/**
 * Copyright (c) 2013-2020 Contributors to the Eclipse Foundation
 *
 * <p> See the NOTICE file distributed with this work for additional information regarding copyright
 * ownership. All rights reserved. This program and the accompanying materials are made available
 * under the terms of the Apache License, Version 2.0 which accompanies this distribution and is
 * available at http://www.apache.org/licenses/LICENSE-2.0.txt
 */
package org.locationtech.geowave.core.geotime.util;

import java.sql.Timestamp;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.geotools.data.Query;
import org.geotools.filter.visitor.NullFilterVisitor;
import org.geotools.util.Converters;
import org.locationtech.geowave.core.geotime.store.query.TemporalConstraints;
import org.locationtech.geowave.core.geotime.store.query.TemporalConstraintsSet;
import org.locationtech.geowave.core.geotime.store.query.TemporalRange;
import org.opengis.filter.And;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.Not;
import org.opengis.filter.Or;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNil;
import org.opengis.filter.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Add;
import org.opengis.filter.expression.Divide;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.NilExpression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.expression.Subtract;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;
import org.opengis.filter.temporal.After;
import org.opengis.filter.temporal.AnyInteracts;
import org.opengis.filter.temporal.Before;
import org.opengis.filter.temporal.Begins;
import org.opengis.filter.temporal.BegunBy;
import org.opengis.filter.temporal.During;
import org.opengis.filter.temporal.EndedBy;
import org.opengis.filter.temporal.Ends;
import org.opengis.filter.temporal.Meets;
import org.opengis.filter.temporal.MetBy;
import org.opengis.filter.temporal.OverlappedBy;
import org.opengis.filter.temporal.TContains;
import org.opengis.filter.temporal.TEquals;
import org.opengis.filter.temporal.TOverlaps;
import org.opengis.temporal.Instant;
import org.opengis.temporal.Period;
import org.opengis.temporal.Position;

/**
 * This class can be used to get Time range from an OpenGIS filter object. GeoWave then uses this
 * time range to perform a spatial intersection query.
 *
 * <p> Only those time elements associated with an index are extracted. At the moment, the adapter
 * only supports temporal indexing on a single attribute or a pair of attributes representing a time
 * range.
 */
public class ExtractTimeFilterVisitor extends NullFilterVisitor {
  private final List<String[]> validParamRanges = new LinkedList<>();

  private boolean approximation = false;

  public ExtractTimeFilterVisitor() {}

  public ExtractTimeFilterVisitor(final TimeDescriptors timeDescriptors) {
    if (timeDescriptors.hasTime()
        && (timeDescriptors.getStartRange() != null)
        && (timeDescriptors.getEndRange() != null)) {
      addRangeVariables(
          timeDescriptors.getStartRange().getLocalName(),
          timeDescriptors.getEndRange().getLocalName());
    }
  }

  public void addRangeVariables(final String start, final String end) {
    validParamRanges.add(new String[] {start, end});
  }

  public TemporalConstraintsSet getConstraints(final Filter filter) {
    final TemporalConstraintsSet constrainsSet = getRawConstraints(filter);
    constrainsSet.setExact(!approximation);
    for (final String[] range : validParamRanges) {
      if (constrainsSet.hasConstraintsFor(range[0]) || constrainsSet.hasConstraintsFor(range[1])) {
        final TemporalConstraints start =
            (constrainsSet.hasConstraintsFor(range[0])) ? constrainsSet.getConstraintsFor(range[0])
                : constrainsSet.getConstraintsFor(range[1]);
        // Note: getConstraints has a side effect that is returns a
        // constraint--full range, if necessary
        // so if start and end are both not specific, the prior line
        // would create the end
        // thus sconstraints and econstraints will be identical
        final TemporalConstraints end =
            (constrainsSet.hasConstraintsFor(range[1])) ? constrainsSet.getConstraintsFor(range[1])
                : start;

        constrainsSet.removeConstraints(range[0], range[1]);
        final TemporalConstraints constraintsForRange =
            constrainsSet.getConstraintsForRange(range[0], range[1]);
        constraintsForRange.replaceWithIntersections(
            new TemporalConstraints(
                new TemporalRange(
                    start.getStartRange().getStartTime(),
                    end.getEndRange().getEndTime()),
                constraintsForRange.getName()));
      }
    }
    return constrainsSet;
  }

  public TemporalConstraintsSet getConstraints(final Query query) {
    return getConstraints(query.getFilter());
  }

  private TemporalConstraintsSet getRawConstraints(final Filter filter) {
    final Object output = filter.accept(this, null);

    if (output instanceof TemporalConstraintsSet) {
      return (TemporalConstraintsSet) output;
    } else if (output instanceof ParameterTimeConstraint) {
      final ParameterTimeConstraint paramConstraint = (ParameterTimeConstraint) output;
      final TemporalConstraintsSet constraintSet = new TemporalConstraintsSet();
      constraintSet.getConstraintsFor(paramConstraint.getName()).replaceWithMerged(paramConstraint);
      return constraintSet;
    }
    return new TemporalConstraintsSet();
  }

  /**
   * Produce an ReferencedEnvelope from the provided data parameter.
   *
   * @param data
   * @return ReferencedEnvelope
   */
  private TemporalConstraints btime(final Object data) {

    if (data == null) {
      return null;
    }
    if (data instanceof Date) {
      return toSet(new TemporalRange((Date) data, (Date) data));
    } else if (data instanceof Timestamp) {
      return toSet(new TemporalRange((Timestamp) data, (Timestamp) data));
    } else if (data instanceof Number) {
      final long val = ((Number) data).longValue();
      return toSet(new TemporalRange(new Date(val), new Date(val)));
    } else if (data instanceof TemporalRange) {
      return toSet((TemporalRange) data);
    } else if (data instanceof TemporalConstraints) {
      return (TemporalConstraints) data;
    } else if (data instanceof Period) {
      // all periods are exclusive
      final Position beginPosition = ((Period) data).getBeginning().getPosition();
      final Position endPosition = ((Period) data).getEnding().getPosition();
      Date s = TemporalRange.START_TIME, e = TemporalRange.START_TIME;

      if (beginPosition.getDate() != null) {
        // make it exclusive on start
        s = new Date(beginPosition.getDate().getTime() + 1);
      } else if (beginPosition.getTime() != null) {
        // make it exclusive on start
        s = new Date(beginPosition.getTime().getTime() + 1);
      }

      if (endPosition.getDate() != null) {
        // make it exclusive on end
        e = new Date(endPosition.getDate().getTime() - 1);
      } else if (endPosition.getTime() != null) {
        // make it exclusive on end
        e = new Date(endPosition.getTime().getTime() - 1);
      }
      if (s.getTime() > e.getTime()) {
        return new TemporalConstraints();
      }
      return toSet(new TemporalRange(s, e));
    } else if (data instanceof Instant) {
      final Position beginPosition = ((Instant) data).getPosition();
      Date s = TemporalRange.START_TIME;
      if (beginPosition.getDate() != null) {
        s = beginPosition.getDate();
      } else if (beginPosition.getTime() != null) {
        s = beginPosition.getTime();
      }
      return toSet(new TemporalRange(s, s));
    }

    final Date convertedDate = Converters.convert(data, Date.class);
    if (convertedDate != null) {
      return btime(convertedDate);
    }
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final ExcludeFilter filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final IncludeFilter filter, final Object data) {
    return new TemporalConstraints();
  }

  private TemporalConstraints toSet(final TemporalRange range) {
    final TemporalConstraints contraints = new TemporalConstraints();
    contraints.add(range);
    return contraints;
  }

  /**
   * Please note we are only visiting literals involved in time.
   *
   * @param expression a literal time
   * @param data unused
   * @return temporal constraints updated to reflect literal
   */
  @Override
  public Object visit(final Literal expression, final Object data) {
    final Object value = expression.getValue();
    return btime(value);
  }

  @Override
  public Object visit(final And filter, final Object data) {
    final TemporalConstraintsSet constraints = new TemporalConstraintsSet();
    for (final Filter f : filter.getChildren()) {
      final Object output = f.accept(this, data);
      if (output instanceof ParameterTimeConstraint) {
        final ParameterTimeConstraint ranges = (ParameterTimeConstraint) output;
        constraints.getConstraintsFor(ranges.getName()).replaceWithIntersections(ranges);
      } else if (output instanceof TemporalConstraintsSet) {
        final TemporalConstraintsSet rangeSet = (TemporalConstraintsSet) output;
        for (final Map.Entry<String, TemporalConstraints> entry : rangeSet.getSet()) {
          constraints.getConstraintsFor(entry.getKey()).replaceWithIntersections(entry.getValue());
        }
      }
    }
    for (final String[] range : validParamRanges) {
      if (constraints.hasConstraintsFor(range[0]) && constraints.hasConstraintsFor(range[1])) {
        final TemporalConstraints start = constraints.getConstraintsFor(range[0]);
        final TemporalConstraints end = constraints.getConstraintsFor(range[1]);
        constraints.removeConstraints(range[0], range[1]);
        // TODO: make this logic more robust
        if (start.getEndRange().getEndTime().after(end.getStartRange().getStartTime())) {
          // does this really make sense? seems like start should always be the start time and end
          // should always be the end time, but perhaps with multiple and's and or's it probably
          // gets complicated such that this is the only working logic
          constraints.getConstraintsForRange(range[0], range[1]).add(
              new TemporalRange(
                  end.getStartRange().getStartTime(),
                  start.getEndRange().getEndTime()));
        } else {
          // if there are multiple non-instersecting ranges, this is
          // an approximation
          approximation |= (start.getRanges().size() > 1) || (end.getRanges().size() > 1);

          constraints.getConstraintsForRange(range[0], range[1]).add(
              new TemporalRange(
                  start.getStartRange().getStartTime(),
                  end.getEndRange().getEndTime()));
        }
      }
    }
    return constraints;
  }

  public boolean isApproximation() {
    return approximation;
  }

  @Override
  public Object visit(final Not filter, final Object data) {
    final Object output = filter.getFilter().accept(this, data);
    if (output instanceof ParameterTimeConstraint) {
      return not((ParameterTimeConstraint) output);
    } else if (output instanceof TemporalConstraintsSet) {
      final TemporalConstraintsSet newRangeSet = new TemporalConstraintsSet();
      final TemporalConstraintsSet rangeSet = (TemporalConstraintsSet) output;
      for (final Map.Entry<String, TemporalConstraints> entry : rangeSet.getSet()) {
        newRangeSet.getConstraintsFor(entry.getKey()).replaceWithMerged(not(entry.getValue()));
      }
      return newRangeSet;
    }
    return output;
  }

  private TemporalConstraints not(final TemporalConstraints constraints) {
    final ParameterTimeConstraint notRanges = new ParameterTimeConstraint(constraints.getName());
    notRanges.empty();

    Date lastMax = TemporalRange.START_TIME;
    for (final TemporalRange range : constraints.getRanges()) {
      if (range.getStartTime().after(TemporalRange.START_TIME)) {
        notRanges.add(new TemporalRange(lastMax, new Date(range.getStartTime().getTime() - 1)));
      }
      lastMax = range.getEndTime();
    }
    if (!constraints.isEmpty()
        && (TemporalRange.END_TIME.after(constraints.getEndRange().getEndTime()))) {
      notRanges.add(new TemporalRange(lastMax, TemporalRange.END_TIME));
    }
    return notRanges;
  }

  @Override
  public Object visit(final Or filter, final Object data) {
    final TemporalConstraintsSet constraints = new TemporalConstraintsSet();
    for (final Filter f : filter.getChildren()) {
      final Object output = f.accept(this, data);
      if (output instanceof ParameterTimeConstraint) {
        final ParameterTimeConstraint ranges = (ParameterTimeConstraint) output;
        constraints.getConstraintsFor(ranges.getName()).replaceWithMerged(ranges);
      } else if (output instanceof TemporalConstraintsSet) {
        final TemporalConstraintsSet rangeSet = (TemporalConstraintsSet) output;
        for (final Map.Entry<String, TemporalConstraints> entry : rangeSet.getSet()) {
          constraints.getConstraintsFor(entry.getKey()).replaceWithMerged(entry.getValue());
        }
      }
    }

    return constraints;
  }

  // t1 > t2
  // t1.start > t2
  // t1 > t2.end
  // t1.start > t2.end
  @Override
  public Object visit(final After after, final Object data) {
    final TemporalConstraints leftResult = btime(after.getExpression1().accept(this, data));
    final TemporalConstraints rightResult = btime(after.getExpression2().accept(this, data));

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property after value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              rightResult.getMaxOr(TemporalRange.START_TIME, 1),
              TemporalRange.END_TIME),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              leftResult.getMinOr(TemporalRange.END_TIME, -1)),
          rightResult.getName());
    }
    // property after property
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final AnyInteracts anyInteracts, final Object data) {
    return new TemporalConstraints();
  }

  // t1 < t2
  // t1.end < t2
  // t1 < t2.start
  // t1.end < t2.start
  // t1.end < t2.start
  @Override
  public Object visit(final Before before, final Object data) {
    final TemporalConstraints leftResult = btime(before.getExpression1().accept(this, data));
    final TemporalConstraints rightResult = btime(before.getExpression2().accept(this, data));

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property before value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              rightResult.getMinOr(TemporalRange.END_TIME, -1)),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              leftResult.getMaxOr(TemporalRange.START_TIME, 1),
              TemporalRange.END_TIME),
          rightResult.getName());
    }
    // property after property
    return new TemporalConstraints();
  }

  // t1 = t2.start
  // t1.start = t2.start and t1.end < t2.end
  @Override
  public Object visit(final Begins begins, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) begins.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) begins.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property begins value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(rightResult.getRanges(), leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              leftResult.getMinOr(TemporalRange.START_TIME, 0),
              TemporalRange.END_TIME),
          rightResult.getName());
    }
    // property begins property
    return new TemporalConstraints();
  }

  // t1.start = t2
  // t1.start = t2.start and t1.end > t2.end
  @Override
  public Object visit(final BegunBy begunBy, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) begunBy.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) begunBy.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property begun by value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              rightResult.getMinOr(TemporalRange.START_TIME, 0),
              TemporalRange.END_TIME),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(leftResult.getRanges(), rightResult.getName());
    }
    // property begins property
    return new TemporalConstraints();
  }

  // t2.start < t1 < t2.end
  // t1.start > t2.start and t1.end < t2.end
  @Override
  public Object visit(final During during, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) during.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) during.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property during value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(rightResult.getRanges(), leftResult.getName());
    }
    // value during property
    else if (rightResult instanceof ParameterTimeConstraint) {
      return rightResult;
    }
    // property during property
    return new TemporalConstraints();
  }

  // t1.end = t2
  // t1.start < t2.start and t1.end = t2.end
  @Override
  public Object visit(final EndedBy endedBy, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) endedBy.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) endedBy.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property ended by value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              rightResult.getMaxOr(TemporalRange.END_TIME, 0)),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(leftResult.getRanges(), rightResult.getName());
    }
    // property ended by property
    return new TemporalConstraints();
  }

  // t1 = t2.end
  // t1.start > t2.start and t1.end = t2.end
  @Override
  public Object visit(final Ends ends, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) ends.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) ends.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property ends value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(rightResult.getRanges(), leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              leftResult.getMaxOr(TemporalRange.END_TIME, 0)),
          rightResult.getName());
    }
    // property ended by property
    return new TemporalConstraints();
  }

  // t1.end = t2.start
  @Override
  public Object visit(final Meets meets, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) meets.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) meets.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property ends value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              rightResult.getMinOr(TemporalRange.END_TIME, 0)),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(rightResult.getName());
    }
    // property ended by property
    return new TemporalConstraints();
  }

  // t1.start = t2.end
  // met by
  @Override
  public Object visit(final MetBy metBy, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) metBy.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) metBy.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property ends value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              rightResult.getMaxOr(TemporalRange.START_TIME, 0),
              TemporalRange.END_TIME),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              leftResult.getMinOr(TemporalRange.END_TIME, 0)),
          rightResult.getName());
    }
    // property ends property
    return new TemporalConstraints();
  }

  // t1.start > t2.start and t1.start < t2.end and t1.end > t2.end
  @Override
  public Object visit(final OverlappedBy overlappedBy, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) overlappedBy.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) overlappedBy.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property overlappedBy value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              rightResult.getMinOr(TemporalRange.START_TIME, 1),
              TemporalRange.END_TIME),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              leftResult.getMaxOr(TemporalRange.END_TIME, -1)),
          rightResult.getName());
    }
    // property overlappedBy property
    return new TemporalConstraints();
  }

  // t1.start < t2 < t1.end
  // t1.start < t2.start and t2.end < t1.end
  @Override
  public Object visit(final TContains contains, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) contains.getExpression1().accept(this, data);

    final TemporalConstraints rightResult =
        (TemporalConstraints) contains.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property contains value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new TemporalConstraints(
          new TemporalRange(
              TemporalRange.START_TIME,
              rightResult.getMaxOr(TemporalRange.END_TIME, -1)),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(leftResult.getRanges(), rightResult.getName());
    }
    // property contains property
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final TEquals equals, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) equals.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) equals.getExpression2().accept(this, data);

    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    // property contains value
    if (leftResult instanceof ParameterTimeConstraint) {
      return rightResult;
    }
    // value contains property
    if (rightResult instanceof ParameterTimeConstraint) {
      return leftResult;
    }
    // property contains property
    return new TemporalConstraints();
  }

  // t1.start < t2.start and t1.end > t2.start and t1.end < t2.end
  @Override
  public Object visit(final TOverlaps overlaps, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) overlaps.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) overlaps.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }
    // according to geotools documentation this is exclusive even though
    // "overlaps" seems it should imply inclusive

    // property overlappedBy value
    if (leftResult instanceof ParameterTimeConstraint) {
      return new TemporalConstraints(
          new TemporalRange(
              TemporalRange.START_TIME,
              rightResult.getMaxOr(TemporalRange.END_TIME, -1)),
          leftResult.getName());
    } else if (rightResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              leftResult.getMaxOr(TemporalRange.START_TIME, -1),
              TemporalRange.END_TIME),
          rightResult.getName());
    }
    // property overlappedBy property
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Id filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final PropertyIsBetween filter, final Object data) {
    final TemporalConstraints propertyExp =
        (TemporalConstraints) filter.getExpression().accept(this, data);

    final TemporalConstraints lowerBound =
        (TemporalConstraints) filter.getLowerBoundary().accept(this, data);
    final TemporalConstraints upperBound =
        (TemporalConstraints) filter.getUpperBoundary().accept(this, data);

    if (propertyExp.isEmpty()) {
      return new TemporalConstraints();
    }

    return new ParameterTimeConstraint(
        new TemporalRange(
            lowerBound.getStartRange().getStartTime(),
            upperBound.getEndRange().getEndTime()),
        propertyExp.getName());
  }

  @Override
  public Object visit(final PropertyIsEqualTo filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              rightResult.getStartRange().getStartTime(),
              rightResult.getEndRange().getEndTime()),
          leftResult.getName());
    } else {
      return new ParameterTimeConstraint(
          new TemporalRange(
              leftResult.getStartRange().getStartTime(),
              leftResult.getEndRange().getEndTime()),
          rightResult.getName());
    }
  }

  @Override
  public Object visit(final PropertyIsNotEqualTo filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }
    if (leftResult instanceof ParameterTimeConstraint) {
      final ParameterTimeConstraint constraints =
          new ParameterTimeConstraint(
              new TemporalRange(
                  TemporalRange.START_TIME,
                  rightResult.getStartRange().getStartTime()),
              leftResult.getName());
      constraints.add(
          new TemporalRange(rightResult.getEndRange().getEndTime(), TemporalRange.END_TIME));
      return constraints;
    } else {
      final ParameterTimeConstraint constraints =
          new ParameterTimeConstraint(
              new TemporalRange(
                  TemporalRange.START_TIME,
                  leftResult.getStartRange().getStartTime()),
              rightResult.getName());
      constraints.add(
          new TemporalRange(leftResult.getEndRange().getEndTime(), TemporalRange.END_TIME));
      return constraints;
    }
  }

  @Override
  public Object visit(final PropertyIsGreaterThan filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }
    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              new Date(rightResult.getStartRange().getStartTime().getTime() + 1),
              TemporalRange.END_TIME),
          leftResult.getName());
    } else {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              new Date(leftResult.getStartRange().getStartTime().getTime() - 1)),
          rightResult.getName());
    }
  }

  @Override
  public Object visit(final PropertyIsGreaterThanOrEqualTo filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(rightResult.getStartRange().getStartTime(), TemporalRange.END_TIME),
          leftResult.getName());
    } else {
      return new ParameterTimeConstraint(
          new TemporalRange(TemporalRange.START_TIME, leftResult.getStartRange().getStartTime()),
          rightResult.getName());
    }
  }

  @Override
  public Object visit(final PropertyIsLessThan filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(
              TemporalRange.START_TIME,
              new Date(rightResult.getStartRange().getStartTime().getTime() - 1)),
          leftResult.getName());
    } else {
      return new ParameterTimeConstraint(
          new TemporalRange(
              new Date(leftResult.getStartRange().getStartTime().getTime() + 1),
              TemporalRange.END_TIME),
          rightResult.getName());
    }
  }

  @Override
  public Object visit(final PropertyIsLessThanOrEqualTo filter, final Object data) {
    final TemporalConstraints leftResult =
        (TemporalConstraints) filter.getExpression1().accept(this, data);
    final TemporalConstraints rightResult =
        (TemporalConstraints) filter.getExpression2().accept(this, data);
    if (leftResult.isEmpty() || rightResult.isEmpty()) {
      return new TemporalConstraints();
    }

    if (leftResult instanceof ParameterTimeConstraint) {
      return new ParameterTimeConstraint(
          new TemporalRange(TemporalRange.START_TIME, rightResult.getStartRange().getStartTime()),
          leftResult.getName());
    } else {
      return new ParameterTimeConstraint(
          new TemporalRange(leftResult.getStartRange().getStartTime(), TemporalRange.END_TIME),
          rightResult.getName());
    }
  }

  @Override
  public Object visit(final PropertyIsLike filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final PropertyIsNull filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final PropertyIsNil filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final BBOX filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Beyond filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Contains filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Crosses filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Disjoint filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final DWithin filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Equals filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Intersects filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Overlaps filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Touches filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Within filter, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visitNullFilter(final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final NilExpression expression, final Object data) {
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Add expression, final Object data) {
    return expression.accept(this, data);
  }

  @Override
  public Object visit(final Divide expression, final Object data) {
    return expression.accept(this, data);
  }

  @Override
  public Object visit(final Function expression, final Object data) {
    // used force full range if the expression contains a time
    // property...which is correct?
    return new TemporalConstraints();
  }

  private boolean validateName(final String name) {
    return true;
  }

  @Override
  public Object visit(final PropertyName expression, final Object data) {
    final String name = expression.getPropertyName();
    if (validateName(expression.getPropertyName())) {
      // for (final String[] range : validParamRanges) {
      // if (range[0].equals(name) || range[1].equals(name)) {
      // return new ParameterTimeConstraint(
      // range[0] + "_" + range[1]);
      // }
      // }
      return new ParameterTimeConstraint(name);
    }
    return new TemporalConstraints();
  }

  @Override
  public Object visit(final Subtract expression, final Object data) {
    return expression.accept(this, data);
  }

  private boolean expressionContainsTime(final Expression expression) {
    return !((TemporalConstraints) expression.accept(this, null)).isEmpty();
  }

  private boolean containsTime(final Function function) {
    boolean yes = false;
    for (final Expression expression : function.getParameters()) {
      yes |= expressionContainsTime(expression);
    }
    return yes;
  }

  private static class ParameterTimeConstraint extends TemporalConstraints {

    public ParameterTimeConstraint(final String name) {
      super(TemporalConstraints.FULL_RANGE, name);
    }

    public ParameterTimeConstraint(final List<TemporalRange> ranges, final String name) {
      super(ranges, name);
    }

    public ParameterTimeConstraint(final TemporalRange range, final String name) {
      super(range, name);
    }

    public TemporalConstraints bounds(final TemporalConstraints other) {
      return other;
    }
  }
}
